package com.xuecheng.media.service.impl;

import com.alibaba.nacos.common.utils.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.execption.XueChengPlusException;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.mapper.MediaProcessMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.model.po.MediaProcess;
import com.xuecheng.media.service.MediaFilesService;
import com.xuecheng.model.PageParams;
import com.xuecheng.model.PageResult;
import com.xuecheng.model.RestResponse;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.codehaus.groovy.tools.shell.IO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * <p>
 * 媒资信息 服务实现类
 * </p>
 *
 * @author itcast
 * @since 2023-03-27
 */
@Slf4j
@Service
public class MediaFilesServiceImpl extends ServiceImpl<MediaFilesMapper, MediaFiles> implements MediaFilesService {

    @Autowired
    private MediaFilesMapper mediaFilesMapper;
    @Autowired
    private MinioClient minioClient;
    @Autowired
    private MediaFilesService mediaFilesService;
    @Autowired
    private MediaProcessMapper mediaProcessMapper;

    //普通文件存储的桶
    @Value("${minio.bucket.files}")
    private String bucket_files;
    //视频文件存储的桶
    @Value("${minio.bucket.videofiles}")
    private String bucket_videofiles;


    @Override
    public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {
        //构建查询条件对象
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(StringUtils.isNotEmpty(queryMediaParamsDto.getFilename()) , MediaFiles::getFilename , queryMediaParamsDto.getFilename());
        queryWrapper.eq(StringUtils.isNotEmpty(queryMediaParamsDto.getFileType()) , MediaFiles::getFileType , queryMediaParamsDto.getFileType());
        queryWrapper.eq(StringUtils.isNotEmpty(queryMediaParamsDto.getAuditStatus()) , MediaFiles::getAuditStatus , queryMediaParamsDto.getAuditStatus());
        //分页对象
        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 查询数据内容获得结果
        Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
        // 获取数据列表
        List<MediaFiles> list = pageResult.getRecords();
        // 获取数据总数
        long total = pageResult.getTotal();
        // 构建结果集
        PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
        return mediaListResult;
    }


    /**
     * 将文件上传到MinIO并将数据保存到数据库
     * @param companyID 机构ID
     * @param uploadFileParamsDto 前端上传过来的文件相关信息
     * @param FilePath 文件路径
     * @param objectName 上传到MinIO的ObjectName
     * @return
     */
    @Override
    public UploadFileResultDto uploadFile(long companyID, UploadFileParamsDto uploadFileParamsDto, String FilePath , String objectName) {
        //拿到文件的MD5值
        String fileMD5 = GetFileMD5(new File(FilePath));
        //获取文件的后缀名
        String filename = uploadFileParamsDto.getFilename();
        String FileSuffix = filename.substring(filename.lastIndexOf("."));
        //获取文件contentType
        String contentType = GetFileContentType(FileSuffix);
        //处理文件名称
        if (StringUtils.isEmpty(objectName)){
            objectName = GetFileObjectName(fileMD5, FileSuffix);
        }
        //上传文件到MinIO
        LoadFileMinIo(bucket_files , objectName , contentType , FilePath);
        //保存到数据库
        MediaFiles mediaFiles = mediaFilesService.SaveFileData(companyID, bucket_files, fileMD5, objectName, uploadFileParamsDto);
        UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
        BeanUtils.copyProperties(mediaFiles , uploadFileResultDto);
        return uploadFileResultDto;
    }

    /**
     * 讲文件信息上传到数据库
     * @param companyID 机构ID
     * @param bucket 桶
     * @param fileMD5 文件的MD5值
     * @param objectName 文件的名称
     * @param paramsDto
     * @return
     */
    @Transactional
    @Override
    public MediaFiles SaveFileData(long companyID , String bucket , String fileMD5 , String objectName , UploadFileParamsDto paramsDto){
        MediaFiles mediaFiles = this.getById(fileMD5);
        if (mediaFiles == null){
            mediaFiles = new MediaFiles();
            BeanUtils.copyProperties(paramsDto , mediaFiles);
            mediaFiles.setId(fileMD5);
            mediaFiles.setCompanyId(companyID);
            mediaFiles.setBucket(bucket);
            mediaFiles.setFilePath(objectName);
            mediaFiles.setFileId(fileMD5);
            mediaFiles.setUrl("/" + bucket + "/" + objectName);
            mediaFiles.setCreateDate(LocalDateTime.now());
            mediaFiles.setStatus("1");
            mediaFiles.setAuditStatus("002003");
            mediaFiles.setFileType("001001");
            mediaFiles.setFileSize(paramsDto.getFileSize());
            int insert = mediaFilesMapper.insert(mediaFiles);
            if (insert <= 0){
                log.info("向数据库插入失败，这里查询到文件已存在,文件ID{},桶{},文件名称{}",fileMD5 ,bucket , objectName);
                throw new XueChengPlusException(paramsDto.getFilename() +" 文件保存失败!");
            }

            //记录待处理任务
            AddFileDisposeTask(mediaFiles);

            return mediaFiles;
        }
        return mediaFiles;
    }


    /**
     * 添加待处理任务
     * @param mediaFiles 媒资文件信息
     */
    private void AddFileDisposeTask(MediaFiles mediaFiles){
        LambdaQueryWrapper<MediaProcess> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(MediaProcess::getFileId , mediaFiles.getFileId());
        Integer count = mediaProcessMapper.selectCount(queryWrapper);
        if (count > 0){
            return;
        }
        //比如：如果AVI格式的视频就加入待处理表
        String filename = mediaFiles.getFilename();
        String substring = filename.substring(filename.lastIndexOf("."));
        String contentType = GetFileContentType(substring);
        //讲来这个video/x-msvideo可以换成从nacos中获取的list，list.con
        if ("video/x-msvideo".equals(contentType)){
            MediaProcess mediaProcess = new MediaProcess();
            BeanUtils.copyProperties(mediaFiles , mediaProcess , "url");
            mediaProcess.setStatus("1");
            mediaProcess.setFailCount(0);
            mediaProcess.setCreateDate(LocalDateTime.now());
            int insert = mediaProcessMapper.insert(mediaProcess);
            if (insert <= 0){
                throw new XueChengPlusException("文件即将转码时失败！");
            }
        }
    }

    /**
     * 上传到MinIO的文件名称
     * @param FileNameMD5
     * @param Suffix
     * @return
     */
    private String GetFileObjectName(String FileNameMD5 , String Suffix){
        if (StringUtils.isNotEmpty(FileNameMD5) && StringUtils.isNotEmpty(Suffix)){
            LocalDateTime now = LocalDateTime.now();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            String format = now.format(formatter);
            String BucketPath = format.replaceAll("-", "/");
            return BucketPath + FileNameMD5 + Suffix;
        }
        throw new XueChengPlusException("获取文件名称失败");
    }

    /**
     * 获取文件的contentType
     * @param FileExtension 文件的后缀名
     * @return
     */
    @Override
    public String GetFileContentType(String FileExtension){
        if (FileExtension == null){
            //如果FileExtension等于空的话给他一个空字符串，不然会报空指针异常
            FileExtension = "";
        }
        ContentInfo mimeTypeMatch = ContentInfoUtil.findExtensionMatch(FileExtension);
        String applicationOctetStream = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if (mimeTypeMatch != null){
            applicationOctetStream = mimeTypeMatch.getMimeType();
            return applicationOctetStream;
        }
        return applicationOctetStream;
    }


    /**
     * 将文件上传到MinIO
     * @param bucket 桶
     * @param objectName  文件名称
     * @param ContentType  文件类型
     * @param FilePath 文件的地址
     */
    @Override
    public boolean LoadFileMinIo(String bucket , String objectName , String ContentType , String FilePath){
        try {
            minioClient.uploadObject(UploadObjectArgs
                    .builder()
                    .bucket(bucket)
                    .object(objectName)
                    .contentType(ContentType)
                    .filename(FilePath)
                    .build());
            return true;
        } catch (Exception e) {
            log.info("文件上传失败,文件名称{},错误信息{}",objectName , e.getMessage());
            throw new XueChengPlusException("上传文件失败");
        }
    }

    /**
     * 获取文件的MD5值
     * @param file 文件路径
     * @return
     */
    private String GetFileMD5(File file){
        try {
            return DigestUtils.md5Hex(new FileInputStream(file));
        } catch (IOException e) {
            log.info("获取文件的MD5失败,文件地址{},错误信息{}",file , e.getMessage());
            throw new XueChengPlusException("获取文件名称失败");
        }
    }


    /**
     * 上传文件前检查文件是否存在
     * @param md5
     * @return
     */
    @Override
    public RestResponse<Boolean> FileUploadCheck(String md5) {
        //在文件表存在，并且在文件系统存在，此文件才存在
        MediaFiles mediaFiles = mediaFilesMapper.selectById(md5);
        if(mediaFiles==null){
            return RestResponse.success(false);
        }
        //查看是否在文件系统存在
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(mediaFiles.getBucket()).object(mediaFiles.getFilePath()).build();
        try {
            InputStream inputStream = minioClient.getObject(getObjectArgs);
            if(inputStream==null){
                //文件不存在
                return RestResponse.success(false);
            }
        }catch (Exception e){
            //文件不存在
            return RestResponse.success(false);
        }
        //文件已存在
        return RestResponse.success(true);
    }

    /**
     * 上传分块文件检查分块文件是否存在
     * @param md5
     * @param nowFile
     * @return
     */
    @Override
    public RestResponse<Boolean> UploadBlockFileCheck(String md5, int nowFile) {
        try {
        //拿到文件的逻辑文件名
        String FileName = GetFileMinioName(md5) + nowFile;
        GetObjectArgs args = GetObjectArgs.builder()
                .bucket(bucket_videofiles)
                .object(FileName)
                .build();
            InputStream object = minioClient.getObject(args);
            if (object == null){
                //分块文件不存在
                return RestResponse.success(false);
            }
        } catch (Exception e) {
            log.info("检查分块文件是否存在时出现异常：文件MD5{},分块文件序号{},异常信息{}",md5,nowFile,e.getMessage());
            //如果出现异常那么文件也是默认文件是不存在的
            return RestResponse.success(false);
        }
        //分块文件已存在
        return RestResponse.success(true);

    }

    /**
     * MinIO文件分块的目录
     * @param md5
     * @return
     */
    private String GetFileMinioName(String md5){
        String FileName;
        if (StringUtils.isNotEmpty(md5)){
            FileName = md5.substring(0 ,1) + "/" + md5.substring(1,2) + "/" + md5 + "/" + "chunk" + "/";
            return FileName;
        }
        throw  new XueChengPlusException("文件名获取失败");
    }


    /**
     * 上传分块文件
     * @param file
     * @param md5
     * @param nowFile
     * @return
     */
    @Override
    public RestResponse UploadBlockFile(String file, String md5, int nowFile) {
        String fileMinioName = GetFileMinioName(md5) + nowFile;
        String contentType = GetFileContentType(null);
        boolean loadFileMinIo = LoadFileMinIo(bucket_videofiles, fileMinioName, contentType, file);
        if (!loadFileMinIo){
           return RestResponse.validfail(false , "文件上传失败");
        }
        return RestResponse.success(true);
    }

    /**
     * 合并文件
     * @param companyID 机构id
     * @param fileMD5 文件的MD5值
     * @param FileCount 分块文件的总条数
     * @param uploadFileParamsDto 保存到数据库的数据
     * @return
     */
    @Override
    public RestResponse MergedBranch(long companyID, String fileMD5, int FileCount, UploadFileParamsDto uploadFileParamsDto) {
        //将分块文件合并
        //通过MD5拿到分块文件的目录
        String blockLogicPath = GetFileMinioName(fileMD5);
        //拿到合并后文件的逻辑路径
            //拿到文件的扩展名
            String filename = uploadFileParamsDto.getFilename();
        String FileExtension = filename.substring(filename.lastIndexOf("."));
        String mergeFileName = MergeFileName(fileMD5, FileExtension);
        try {
            boolean mergeBlockFile = MergeBlockFile(FileCount, bucket_videofiles, blockLogicPath, mergeFileName);
            if (!mergeBlockFile){
                return RestResponse.validfail(false , "合并文件失败");
            }
        } catch (Exception e) {
            log.info("合并文件出错:bucket{},合并后的文件名称{},错误信息{}",bucket_videofiles,mergeFileName,e.getMessage());
            return RestResponse.validfail(false , "合并文件失败");
        }

        //将文件信息存储到数据库
        //拿到合并后的文件大小
        long minIoMergeFileSize = GetMinIoMergeFileSize(bucket_videofiles , mergeFileName);
        uploadFileParamsDto.setFileSize(minIoMergeFileSize);
        MediaFiles mediaFiles = mediaFilesService.SaveFileData(companyID, bucket_videofiles, fileMD5, mergeFileName, uploadFileParamsDto);
        if (mediaFiles == null){
            return RestResponse.validfail(false , "保存视频信息失败");
        }

        //删除分块文件
        try {
            DeleteBlockFile(FileCount , bucket_videofiles , blockLogicPath);
        } catch (Exception e) {
            log.info("删除分块文件出错:bucket{},分块文件目录{},错误信息{}",bucket_videofiles,blockLogicPath,e.getMessage());
            return RestResponse.validfail(false , "删除分块文件失败");
        }
        return RestResponse.success(true);
    }




    /**
     * 合并完成后删除分块文件
     * @param BucketFileCount 分块文件的总条数
     * @param Bucket 桶
     * @param BlockFileObjectName 分块文件的路径
     * @throws Exception
     */
    private void DeleteBlockFile(int BucketFileCount , String Bucket ,String BlockFileObjectName) throws Exception{
        List<DeleteObject> deleteObjectList = Stream.iterate(0, i -> ++i)
                .limit(BucketFileCount)
                .map(i -> new DeleteObject(BlockFileObjectName + i))
                .collect(Collectors.toList());
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(Bucket).objects(deleteObjectList).build();
        Iterable<Result<DeleteError>> removeObjects = minioClient.removeObjects(removeObjectsArgs);
        for (Result<DeleteError> deleteErrorResult : removeObjects) {
            deleteErrorResult.get();
        }
    }


    /**
     * 获取合并后的文件大小
     * @param Bucket 桶
     * @param MergeFilePath 合并后的文件路径
     * @return
     */
   public long GetMinIoMergeFileSize(String Bucket , String MergeFilePath)  {
       StatObjectResponse statObjectResponse;
       try {
           statObjectResponse = minioClient.statObject(StatObjectArgs
                   .builder()
                   .bucket(Bucket)
                   .object(MergeFilePath)
                   .build());
       } catch (Exception e) {
           return 0;
       }
       return statObjectResponse.size();
    }

    /**
     * 拿到合并文件的逻辑名称
     * @param FileMD5 文件MD5值
     * @param FileExtension 扩展名
     * @return
     */
    private String MergeFileName(String FileMD5 , String FileExtension){
        return FileMD5.substring(0 , 1) + "/" + FileMD5.substring(1 , 2) + "/" + FileMD5 + "/" + FileMD5 + FileExtension;
    }



    /**
     * 807cecaa7e895514f407970ffc6c8b10
     *  7
     */

    /***
     * 合并分块文件
     * @param BlockFileCount 分块文件的总条数
     * @param Bucket 桶
     * @param BlockFilePath 分块文件路径
     * @param MergeFileName 合并文件名称
     * @return
     * @throws Exception
     */
    private boolean MergeBlockFile(int BlockFileCount , String Bucket ,String BlockFilePath, String MergeFileName) throws Exception{
        List<ComposeSource> sources = Stream.iterate(0, i -> ++i)
                .limit(BlockFileCount)
                .map(i -> ComposeSource.builder().bucket(Bucket).object(BlockFilePath + i).build())
                .collect(Collectors.toList());
        ComposeObjectArgs args = ComposeObjectArgs.builder().bucket(Bucket).sources(sources).object(MergeFileName).build();
        ObjectWriteResponse objectWriteResponse = minioClient.composeObject(args);
        if (objectWriteResponse.object() == null) {
            return false;
        }
        return true;
    }


    /**
     *
     * @param bucket 桶
     * @param ObjectName 文件名称
     * @return 返回File对象，方便删除零时文件
     */
    @Override
    public File DownloadMinIOFile(String bucket, String ObjectName) {
        InputStream getObjectResponse = null;
        File tempFile = null;
        FileOutputStream fileOutputStream = null;
         try {   //从MinIO下载文件
             getObjectResponse = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(ObjectName).build());
             //创建临时文件来存放MinIO下载下来的文件
              tempFile = File.createTempFile(bucket, ".mp4");
             //将文件写入临时文件
             fileOutputStream = new FileOutputStream(tempFile);
             getObjectResponse.transferTo(fileOutputStream);
             //关闭流
             getObjectResponse.close();
             fileOutputStream.close();
             //返回文件对象
             return tempFile;
         } catch (Exception e){
             //如果出现异常就把文件删掉
             log.info("下载文件失败:bucket:{},ObjectName:{}",bucket , ObjectName);
             return null;
         }
    }


    /**
     *
     * @param mediaID
     * @return
     */
    @Override
    public MediaFiles GetMediaFilesByID(String mediaID) {
        MediaFiles mediaFiles = mediaFilesService.getById(mediaID);
        return mediaFiles;
    }

}
